/*
 * PKCS15CardService contains methods for the communication with PKCS#15 compatible card.
 * PKCS15CardService extends JSmexCardService and implements all its abstract methods.
 *
 * Created on 3. Januar 2007, 16:10
 *
 *  This file is part of JSmex.
 *
 *  JSmex is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  JSmex is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Foobar; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

package jsmex.cardservice;

import java.util.Vector;
import jsmex.function.FileProperties;
import jsmex.JSmexTools;
import jsmex.function.SmartCardFile;
import opencard.core.terminal.CardTerminalException;
import opencard.core.terminal.CommandAPDU;
import opencard.core.terminal.ResponseAPDU;
import opencard.core.util.HexString;

/**
 *
 * @author Tobias Senger
 */
public class PKCS15CardService extends JSmexCardService{
    
    private static final byte CLASS_BYTE = (byte)0xC0;  // jedenfalls bei Cryptoflex-Karten. ggf. haben andere Hersteller andere
    private static CommandAPDU command = new CommandAPDU(50); //50 Bytes are enough for the most commands
    private static ResponseAPDU response;
    private static byte[] selectedFID = new byte[2];
    private byte[] pkcs15Fid = null;
    
    /** Creates a new instance of PKCS15CardService */
    public PKCS15CardService() {
        super();
    }
    
    /**
     * This function selects a file (DF or EF)
     * @param fid The FID to select in an byte-array.
     * @return ResponseAPDU of the command.
     */
    public ResponseAPDU selectFID(byte[] fid) {
        byte[] select_cmd= {CLASS_BYTE, (byte)0xA4, (byte)0x00, (byte)0x00, (byte)fid.length};
        byte[] get_response = { CLASS_BYTE, (byte)0xC0, (byte)0x00, (byte)0x00 };
        
        command.setLength(0);
        command.append(select_cmd);
        command.append(fid);
        try {
            response = sendCommandAPDU(command);
            if (response.sw1() == (byte)0x61)  {
                command.setLength(0);
                command.append(get_response);
                command.append(response.sw2());
                response = sendCommandAPDU(command);
                if (response.sw() == 0x9000) {
                    selectedFID[0] = (byte)response.getByte(4);
                    selectedFID[1] = (byte)response.getByte(5);
                }
            }
        } catch (CardTerminalException cte) {
            lt.warning("method selectFID(byte[] fid) throws a CardTerminalException.",this);
        }
        lt.info("called method selectFID(fid: "+HexString.hexify(fid)+"). returns SW: "+HexString.hexify(response.sw1())+" "+HexString.hexify(response.sw2()),this);
        return response;
    }
    
    /**
     * This function selects a file (DF or EF)
     * @param fid The FID to select in a String.
     * @return ResponseAPDU of the command.
     */
    public ResponseAPDU selectFID(String fid) {
        lt.info("called method selectFID(String fid)",this);
        return selectFID(JSmexTools.parseHexString(fid));
    }
    
    /** This method implements the abstract method from JSmexCardService.
     *  It selects the MF of the Geldkarte.
     *
     * @return Returns 'true' if everything went well,
     * otherwise 'false' will be returned.
     */
    public boolean selectMF() {
        
        if (selectFID("3F 00").sw()==0x9000) {
            lt.info("called method selectMF(). return is \"true\"", this);
            return true;
        } else {
            lt.info("called method selectMF(). return is \"false\"", this);
            return false;
        }
    }
    
    /** This method implements the abstract method from JSmexCardService.
     *  It select the a EF whose FID comes in parameter fid.
     *
     * @param fid The FID to be selected in an byte-array.
     * @return Returns the ReponseAPDU of the select commando.
     */
    public ResponseAPDU selectEF(byte[] fid) {
        lt.info("called method selectEF("+HexString.hexify(fid)+")",this);
        return selectFID(fid);
    }
    
    /** This method implements the abstract method from JSmexCardService.
     *  It select the a DF whose FID comes in parameter fid.
     *
     * @param fid The FID to be selected in an byte-array.
     * @return Returns the ReponseAPDU of the select commando.
     */
    public ResponseAPDU selectDF(byte[] fid) {
        lt.info("called method selectDF("+HexString.hexify(fid)+")",this);
        return selectFID(fid);
    }
    
    /**This method checks if the EFs and DFs which are get in parameter filearray.
     * If the file exist, an new instance of <code>SmartCardFile</code> will builded.
     * All founded SmartCardFiles will be returned in an array.
     *
     * @param filearray A two-dimensional array which contains FIDs and names of EFs and DFs.
     * @return An array of SmartCardFile.
     */
    private SmartCardFile[] buildFileList(String[][] filearray) {
        
        ResponseAPDU resp = null;
        SmartCardFile[] scfiles = null;
        Vector filevec = new Vector();
        boolean addDfPKCS15 = false;
        
        lt.info("called method buildFileList(String[][] filearray)", this);
        
        for (int i=0; i<filearray.length;i++){
            try {
                resp = selectFID(filearray[i][1]);    //erstmal testen ob es das File auch wirklich gibt...
                if (resp.sw1()==(byte)0x90) { // Wenn ja die eigenschaften herausfinden
                    
                    FileProperties properties = getProperties(resp);
                    filevec.add(new SmartCardFile(filearray[i][0], properties)); // und wird ein neues File erzeugt und in einem Vektor abgelegt.
                    
                    if (filearray[i][0].equals("EF DIR")) {
                        resp = readBinary((byte)0,(byte)0,(byte)properties.getFileSize());
                        pkcs15Fid = getPKCS15Fid(resp);
                        addDfPKCS15 = true;
                    }
                    
                    if (filearray[i][0].equals("EF ODF")) {
                        SmartCardFile[] files = getODFFiles(properties);
                        for (int j=0;j<files.length;j++) filevec.add(files[j]);
                    }
                    
                }
            } catch (Exception exc) {
                exc.printStackTrace();
            }
        }
        if (pkcs15Fid!=null&&addDfPKCS15) {        //add the DF PKCS#15
            resp = selectFID(pkcs15Fid);
            filevec.add(new SmartCardFile("DF PKCS#15",getProperties(resp)));
            addDfPKCS15 = false;
            selectMF();
        }
        
        scfiles = new SmartCardFile[filevec.size()];  // Länge für das Array aus Vectorgröße ermittel
        scfiles =  (SmartCardFile[]) filevec.toArray(scfiles); // Inhalt des Vectors in ein Array umwandeln
        return scfiles;
    }
    
    /**
     * This method searchs in the EF_ODF for files and returns an array with all founded SmartCardFiles.
     * @param prop The properties instance from the EF_ODF
     * @return An SmartCardFile array.
     */
    private SmartCardFile[] getODFFiles(FileProperties prop) {  //prop are the properties from the EF_ODF
        
        ResponseAPDU resp;
        Vector vec = new Vector();
        SmartCardFile[] files = null;
        ODF_Entry[] entries = null;
        
        lt.info("called method getODFFiles(FileProperties prop).", this);
        
        if (prop.getTof()==1) {
            resp = readBinary((byte)0,(byte)0,(byte)prop.getFileSize());
            entries = decodeODF(resp);
        }
        
        if (prop.getTof()==2||prop.getTof()==4)	{
            int numberOfRecords =prop.getRecordsCount();
            
            for (int i=1;i<=numberOfRecords;i++) {
                resp = readRecord((byte)i,(int)prop.getRecordLength());
                ODF_Entry[] temp = decodeODF(resp);
                if (temp!=null) vec.add(temp[0]);
            }
            
            entries = new ODF_Entry[vec.size()];
            entries = (ODF_Entry[])vec.toArray();
        }
        
        files = new SmartCardFile[entries.length];
        for (int i=0;i<entries.length;i++) {
            resp = selectFID(entries[i].getFid());
            switch (entries[i].getType()) {
                case 0: files[i] = new SmartCardFile("EF PrKDF",getProperties(resp)); break;
                case 1: files[i] = new SmartCardFile("EF PuKDF",getProperties(resp)); break;
                case 3: files[i] = new SmartCardFile("EF CDF",getProperties(resp)); break;
                case 4: files[i] = new SmartCardFile("EF CDF",getProperties(resp)); break;
                case 5: files[i] = new SmartCardFile("EF DODF",getProperties(resp)); break;
                case 6: files[i] = new SmartCardFile("EF AODF",getProperties(resp)); break;
                case 7: files[i] = new SmartCardFile("EF CDF",getProperties(resp)); break;
                case 8: files[i] = new SmartCardFile("EF AODF",getProperties(resp)); break;
                default: files[i] = new SmartCardFile("EF ???",getProperties(resp)); break;
            }
            
        }
        
        return files;
    }
    
    /** This method reads out the properties from the ResponseAPDU from a selectFID() command
     *
     * @param resp The ResponseAPDU from a selectFID() command
     * @return a instance of FileProperties
     */
    private FileProperties getProperties(ResponseAPDU resp) {
        
        lt.info("called method getProperties(ResponseAPDU resp)", this);
        
        if (resp.getByte(6)!=0x38) { // Response from a EF
            
            int efFileSize = resp.getByte(2)*0xFF+resp.getByte(3);
            
            byte [] fid = new byte[2];
            fid[0] = (byte) resp.getByte(4);
            fid[1] = (byte) resp.getByte(5);
            
            int increase_decrease_restriction = resp.getByte(7) >>> 6;
            // 0: no decrease, no increase
            // 1: decrease only
            // 2: increase only
            // 3: both decrease and increase
            
            int readSeekAccess = (resp.getByte(8)&0xF0) >>> 4;
            int updateDecreaseAccess = resp.getByte(8)&0x0F;
            int increaseAccess = resp.getByte(9)&0xF0 >>> 4;
            int createRecordAccess = resp.getByte(9)&0x0F;
            int rehabilitateAccess = (resp.getByte(10)&0xF0) >>> 4;
            int invalidateAccess = resp.getByte(10)&0x0F;
            
            boolean isInvalidated;
            if ((resp.getByte(11)&0x01)==0x01) isInvalidated = false;
            else isInvalidated = true;
            
            int fileStructure = resp.getByte(6); // 1:transparent; 2:linear fixed: 4:linear variable; 6:cyclic
            
            int recordLength = 0;
            if (resp.getByte(12)==2) recordLength = resp.getByte(14);
            
            int records = 0;
            if (recordLength!=0) records = efFileSize/recordLength;
            
            FileProperties fp = new FileProperties(efFileSize, fileStructure, recordLength, records, fid, (byte)readSeekAccess, (byte)updateDecreaseAccess);
            return fp;
        } else if (resp.getByte(6)==0x38) { // resp from DF
            
            int free_memory = resp.getByte(2)*256+resp.getByte(3); // free space available in DF
            
            byte [] fid = new byte[2];
            fid[0] = (byte) resp.getByte(4);
            fid[1] = (byte) resp.getByte(5);
            
            int dirNextAccess = (resp.getByte(8)&0xF0) >>> 4;
            int deleteFileAccess = resp.getByte(9)&0xF0 >>> 4;
            int createFileAccess = resp.getByte(9)&0x0F;
            
            boolean isInvalidated;
            if ((resp.getByte(11)&0x01)==0x01) isInvalidated = false;
            else isInvalidated = true;
            
            boolean CHV1_isActive;
            if ((resp.getByte(13)>>>7)==0x01) CHV1_isActive = true;
            else CHV1_isActive = false;
            
            int numberOfDFs = resp.getByte(14);
            int numberOfEFs = resp.getByte(15);
            
            int numberOfPins = resp.getByte(17);
            
            int remainingCHV1Attemps = 0;
            int remainingPUK1Attemps = 0;
            int remainingCHV2Attemps = 0;
            int remainingPUK2Attemps = 0;
            boolean CHV2_isActive;
            
            if (resp.getByte(12)>=7) {
                remainingCHV1Attemps = resp.getByte(18);
                remainingPUK1Attemps = resp.getByte(19);
                if (resp.getByte(12)>=9) {
                    remainingCHV2Attemps = resp.getByte(20)&07;
                    remainingPUK2Attemps = resp.getByte(21)&07;
                }
            }
            
            FileProperties fp = new FileProperties(free_memory, 0x38, 0, 0, fid, (byte)0x00, (byte)0x0F);
            return fp;
        } else return null;
    }
    
    /**
     * THis method searchs in the EF_DIR for the PKCS#15-AID and gets the FID from the PKCS#15 DF.
     * @param resp The ResponseAPDU which contains the data from the EF_DIR
     * @return A byte-array which contains the FID from the PKCS#15 DF.
     */
    private byte[] getPKCS15Fid(ResponseAPDU resp) {
        
        byte[] PKCS15_AID = { (byte)0xA0, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x63, (byte)0x50, (byte)0x4B, (byte)0x43, (byte)0x53, (byte)0x2D, (byte)0x31, (byte)0x35 };
        byte[] fid;
        
        for (int i=0;i<=resp.getLength();i++) {
            if (resp.getByte(i)==(byte)0x61) {
                for (int j=i;j<=resp.getLength();j++) {
                    if (resp.getByte(j)==(byte)0x4F) {
                        for (int k=0;k<=11;k++) {
                            if ((byte)resp.getByte(j+2+k)!=PKCS15_AID[k]) return null;
                        }
                        for (int l=j+11;l<=resp.getLength();l++) {
                            if ((byte)resp.getByte(l)==(byte)0x51) {
                                fid = new byte[resp.getByte(l+1)];
                                for (int k=0;k<fid.length;k++) {
                                    fid[k]=(byte)resp.getByte(l+2+k);
                                }
                                byte[] returnfid = new byte[2];
                                returnfid[0] = fid[2];
                                returnfid[1] = fid[3];
                                
                                lt.info("called method getPKCS15Fid(ResponseAPDU resp). returns: "+HexString.hexify(returnfid), this);
                                return returnfid;
                            }
                        }
                        
                    }
                    
                }
            }
        }
        lt.info("called method getPKCS15Fid(ResponseAPDU resp). returns: 'null'", this);
        return null;
    }
    
    /**This method returns an array of SmartCardFile which contains all founded
     * files under the DF which ist specified in parameter scfile.
     *
     * @param scfile Contains a SmartCardFile instance of a DF.
     * @return An array of SmartCardFile.
     */
    public SmartCardFile[] getFileList(SmartCardFile scfile) {
        SmartCardFile[] files = null;
        int i;
        
        lt.info("called method getFileList(SmartCardFile scfile). scfile name is"+scfile.toString()+". scfile FID is"+HexString.hexify(scfile.fp.getFID()), this);
        
        if (scfile.fp.getFID()[0]==(byte)0x3F && scfile.fp.getFID()[1]==(byte)0x00) {                 //---------MF--------------
            String[][] mffiles = {{"EF DIR","2F 00"},{"EF CHV1","00 00"},{"EF Key Int","00 01"},{"EF ICC SN","00 02"},{"EF Key Ext","00 11"},{"EF RSA","00 12"},{"EF CHV2","01 00"}};
            
            try {
                selectMF();
            } catch (Exception exc) {}
            
            return buildFileList(mffiles);
        }
        
        else if (scfile.fp.getFID()[0]==pkcs15Fid[0] && scfile.fp.getFID()[1]==pkcs15Fid[1]) {            //---------DF PKCS#15--------------
            String[][] pkcs15files = {{"EF ODF","50 31"},{"EF TokenInfo","50 32"},{"EF UnusedSpace","50 33"}};
            try {
                selectFID(pkcs15Fid);
            } catch (Exception exc) {}
            
            return buildFileList(pkcs15files);
        }
        
        else return files;
        
    }
    
    /**
     * This method decodes a ODF and returns an array of ODF_Entry which contains all founded entries.
     * @param resp The ResponseAPDU which contains the data from the ODF.
     * @return An array of ODF_Entry.
     */
    private ODF_Entry[] decodeODF(ResponseAPDU resp) {
        
        int p = 0;
        int type;
        Vector entryvec = new Vector();
        
        for (p=0;p<resp.getLength();p++) {
            if ((resp.getByte(p)&0xA0)==0xA0) {
                type = resp.getByte(p)&0x0F;
                byte[] part = new byte[resp.getByte(p+1)];
                for (int i=0;i<part.length;i++) {
                    part[i] = (byte)resp.getByte(p+2+i);
                }
                entryvec.add(new ODF_Entry(type,part));
                p += resp.getByte(p+1)+1;
            }
        }
        ODF_Entry[] entries = new ODF_Entry[entryvec.size()];  // Länge für das Array aus Vectorgröße ermittel
        entries =  (ODF_Entry[]) entryvec.toArray(entries); // Inhalt des Vectors in ein Array umwandeln
        
        lt.info("called method decodeODF(ResponseAPDU resp). returns "+entries.length+" ODF_Entries", this);
        return entries;
    }
    
    /**
     * Reads the current selected tranparent EF
     * @param oh offset high
     * @param ol offset low
     * @param length length of data to read
     * @return ResponseAPDU
     */
    
    public ResponseAPDU readBinary(byte oh, byte ol, byte length) {
        byte[] rb_cmd = { CLASS_BYTE, (byte)0xB0, (byte)oh, (byte)ol, (byte)length};
        command.setLength(0);
        command.append(rb_cmd);
        try {
            response = sendCommandAPDU(command);
        } catch (CardTerminalException cte) {
            System.out.println("CardTerminalException: ");
            System.out.println(cte.getMessage() );
        }
        lt.info("called method readBinary(oh: "+HexString.hexify(oh)+", ol: "+HexString.hexify(ol)+", length: "+HexString.hexify(length)+
                ")."+" returns SW: "+HexString.hexify(response.sw1())+" "+HexString.hexify(response.sw2()),this);
        return response;
    }
    
    /**
     * At the moment this method isn't in use.
     * @param recno
     * @param length
     * @return null
     */
    public ResponseAPDU readRecord(byte recno, int length) {
        return null;
    }
    
    /**
     * At the moment this method isn't in use.
     * @param chvindication
     * @param chv
     * @return null
     */
    public ResponseAPDU verifyCHV(byte chvindication, String chv) {
        return null;
    }
    
    /**
     * At the moment this method isn't in use.
     * @param chvindicator
     * @return false
     */
    public boolean chvIsVerified(int chvindicator) {
        return false;
    }
    
    /** This method implements the method of JSmexCardService.
     *  It checks if there is an EF or DF with the fid, which comes in fidString,
     *  the currently selected DF. If a file was found, the method returns a new SmartCardFile
     *  instance for the founded file. Else it return null.
     *
     * @param fidString a String which contains the FID
     * @return a instance of SmartCardFile or null
     */
    public SmartCardFile getSmartCardFile(String fidString) {
        
        byte[] fid = JSmexTools.parseHexString(fidString);
        
        lt.info("called method getSmartCardFile(fidString: "+fidString+")",this);
        
        ResponseAPDU resp = selectDF(fid);
        String name;
        
        if (resp.sw()!=0x9000) {
            resp = selectEF(fid);
            if (resp.sw()!=0x9000) return null;
            
        }
        
        FileProperties fp = getProperties(resp);
        
        if (fp.isDF()) name = new String("DF ("+fidString+")");
        else name = new String("EF ("+fidString+")");
        
        SmartCardFile scfile = new SmartCardFile(name ,fp);
        return scfile;
    }
    
    /**
     * Updates the current selected transparent EF
     * @param oh offset high
     * @param ol offset low
     * @param data Data to write
     * @return ResponseAPDU
     */
    public ResponseAPDU updateBinary(byte oh, byte ol, byte[] data) {
        byte[] ub_cmd = { CLASS_BYTE, (byte)0xD6, (byte)oh, (byte)ol, (byte)data.length};
        CommandAPDU cmd = new CommandAPDU(6+data.length);
        cmd.setLength(0);
        cmd.append(ub_cmd);
        cmd.append(data);
        
        try {
            response = sendCommandAPDU(cmd);
        } catch (CardTerminalException cte) {
            lt.warning("method updateBinary(byte oh, byte ol, byte[] data) throws CardTerminalExecption.",this);
        }
        lt.info("called method updateBinary(byte oh, byte ol, byte[] data)."+" returns SW: "+HexString.hexify(response.sw1())+" "+HexString.hexify(response.sw2()),this);
        return response;
    }
    
    /**This method implements the method of JSmexCardService.
     * It updates one complete record in the current linear or cyclic EF.
     * @param recno record number to update
     * @param data data to write
     * @return ResponseAPDU
     */
    public ResponseAPDU updateRecord(byte recno, byte[] data) {
        byte[] ur_cmd = { CLASS_BYTE, (byte)0xDC, (byte)recno, (byte)0x04, (byte)data.length};
        CommandAPDU cmd = new CommandAPDU(6+data.length);
        cmd.setLength(0);
        cmd.append(ur_cmd);
        cmd.append(data);
        try {
            response = sendCommandAPDU(cmd);
        } catch (CardTerminalException cte) {
            lt.warning("method updateRecord(byte recno, byte[] data) throws a CardTerminalException.",this);
        }
        lt.info("called method updateRecord((byte recno, byte[] data). returns SW: "+HexString.hexify(response.sw1())+" "+HexString.hexify(response.sw2()),this);
        return response;
    }
    
}
